<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<link rel="import" href="settings.html">
<script>
(function() {
  'use strict';

    var DomApi = Polymer.DomApi.ctor;
    var Settings = Polymer.Settings;

    /**
     * DomApi.EffectiveNodesObserver tracks changes to an element's
     * effective child nodes, the same list returned from
     * `Polymer.dom(node).getEffectiveChildNodes()`.
     * It is not meant to be used directly; it is used by
     * `Polymer.dom(node).observeNodes(callback)` to observe changes.
     */
    DomApi.EffectiveNodesObserver = function(domApi) {
      this.domApi = domApi;
      this.node = this.domApi.node;
      this._listeners = [];
    };

    DomApi.EffectiveNodesObserver.prototype = {

      addListener: function(callback) {
        if (!this._isSetup) {
          this._setup();
          this._isSetup = true;
        }
        var listener = {fn: callback, _nodes: []};
        this._listeners.push(listener);
        this._scheduleNotify();
        return listener;
      },

      removeListener: function(handle) {
        var i = this._listeners.indexOf(handle);
        if (i >= 0) {
          this._listeners.splice(i, 1);
          handle._nodes = [];
        }
        if (!this._hasListeners()) {
          this._cleanup();
          this._isSetup = false;
        }
      },

      _setup: function() {
        this._observeContentElements(this.domApi.childNodes);
      },

      _cleanup: function() {
        this._unobserveContentElements(this.domApi.childNodes);
      },

      _hasListeners: function() {
        return Boolean(this._listeners.length);
      },

      _scheduleNotify: function() {
        if (this._debouncer) {
          this._debouncer.stop();
        }
        this._debouncer = Polymer.Debounce(this._debouncer,
          this._notify);
        this._debouncer.context = this;
        Polymer.dom.addDebouncer(this._debouncer);
      },

      notify: function() {
        if (this._hasListeners()) {
          this._scheduleNotify();
        }
      },

      _notify: function() {
        this._beforeCallListeners();
        this._callListeners();
      },

      _beforeCallListeners: function() {
        this._updateContentElements();
      },

      _updateContentElements: function() {
        this._observeContentElements(this.domApi.childNodes);
      },

      _observeContentElements: function(elements) {
        for (var i=0, n; (i < elements.length) && (n=elements[i]); i++) {
          if (this._isContent(n)) {
            n.__observeNodesMap = n.__observeNodesMap || new WeakMap();
            if (!n.__observeNodesMap.has(this)) {
              n.__observeNodesMap.set(this, this._observeContent(n));
            }
          }
        }
      },

      _observeContent: function(content) {
        var self = this;
        var h = Polymer.dom(content).observeNodes(function() {
          self._scheduleNotify();
        });
        h._avoidChangeCalculation = true;
        return h;
      },

      _unobserveContentElements: function(elements) {
        for (var i=0, n, h; (i < elements.length) && (n=elements[i]); i++) {
          if (this._isContent(n)) {
            h = n.__observeNodesMap.get(this);
            if (h) {
              Polymer.dom(n).unobserveNodes(h);
              n.__observeNodesMap.delete(this);
            }
          }
        }
      },

      _isContent: function(node) {
        return (node.localName === 'content');
      },

      _callListeners: function() {
        var o$ = this._listeners;
        var nodes = this._getEffectiveNodes();
        for (var i=0, o; (i < o$.length) && (o=o$[i]); i++) {
          var info = this._generateListenerInfo(o, nodes);
          if (info || o._alwaysNotify) {
            this._callListener(o, info);
          }
        }
      },

      _getEffectiveNodes: function() {
        return this.domApi.getEffectiveChildNodes()
      },

      _generateListenerInfo: function(listener, newNodes) {
        if (listener._avoidChangeCalculation) {
          return true;
        }
        var oldNodes = listener._nodes;
        var info = {
          target: this.node,
          addedNodes: [],
          removedNodes: []
        };
        var splices = Polymer.ArraySplice.calculateSplices(newNodes, oldNodes);
        // process removals
        for (var i=0, s; (i<splices.length) && (s=splices[i]); i++) {
          for (var j=0, n; (j < s.removed.length) && (n=s.removed[j]); j++) {
            info.removedNodes.push(n);
          }
        }
        // process adds
        for (i=0, s; (i<splices.length) && (s=splices[i]); i++) {
          for (j=s.index; j < s.index + s.addedCount; j++) {
            info.addedNodes.push(newNodes[j]);
          }
        }
        // update cache
        listener._nodes = newNodes;
        if (info.addedNodes.length || info.removedNodes.length) {
          return info;
        }
      },

      _callListener: function(listener, info) {
        return listener.fn.call(this.node, info);
      },

      enableShadowAttributeTracking: function() {}

    };

    if (Settings.useShadow) {

      var baseSetup = DomApi.EffectiveNodesObserver.prototype._setup;
      var baseCleanup = DomApi.EffectiveNodesObserver.prototype._cleanup;

      Polymer.Base.mixin(DomApi.EffectiveNodesObserver.prototype, {

        _setup: function() {
          if (!this._observer) {
            var self = this;
            this._mutationHandler = function(mxns) {
              if (mxns && mxns.length) {
                self._scheduleNotify();
              }
            };
            this._observer = new MutationObserver(this._mutationHandler);
            this._boundFlush = function() {
              self._flush();
            }
            Polymer.dom.addStaticFlush(this._boundFlush);
            // NOTE: subtree true is way too aggressive, but it easily catches
            // attribute changes on children. These changes otherwise require
            // attribute observers on every child. Testing has shown this
            // approach to be more efficient.
            // TODO(sorvell): do we still need to include an option to defeat
            // attribute tracking?
            this._observer.observe(this.node, { childList: true });
          }
          baseSetup.call(this);
        },

        _cleanup: function() {
          this._observer.disconnect();
          this._observer = null;
          this._mutationHandler = null;
          Polymer.dom.removeStaticFlush(this._boundFlush);
          baseCleanup.call(this);
        },

        _flush: function() {
          if (this._observer) {
            this._mutationHandler(this._observer.takeRecords());
          }
        },

        enableShadowAttributeTracking: function() {
          if (this._observer) {
            // provoke all listeners needed for <content> observation
            // to always call listeners when no-op changes occur (which may
            // affect lower distributions.
            this._makeContentListenersAlwaysNotify();
            this._observer.disconnect();
            this._observer.observe(this.node, {
              childList: true,
              attributes: true,
              subtree: true
            });
            var root = this.domApi.getOwnerRoot();
            var host = root && root.host;
            if (host && Polymer.dom(host).observer) {
              Polymer.dom(host).observer.enableShadowAttributeTracking();
            }
          }
        },

        _makeContentListenersAlwaysNotify: function() {
          for (var i=0, h; i < this._listeners.length ; i++) {
            h = this._listeners[i];
            h._alwaysNotify = h._isContentListener;
          }
        }

      });

    }

})();

</script>
